use super::*;
use crate::{
    ColumnSample, Land,
    util::{LOCALITY, RandomField, Sampler},
};
use common::terrain::{Block, BlockKind};
use enumset::EnumSet;
use rand::prelude::*;
use strum::IntoEnumIterator;
use util::sprites::PainterSpriteExt;
use vek::*;

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum RoadLights {
    Default,
    Terracotta,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum RoadMaterial {
    Dirt,
    Cobblestone,
    Sandstone,
    Marble,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub struct RoadKind {
    pub lights: RoadLights,
    pub material: RoadMaterial,
}

impl RoadKind {
    /// Intended to be placed at `riverless_alt`.
    pub fn place_light(&self, pos: Vec3<i32>, dir: Dir, painter: &Painter) {
        let wood_corner = Fill::Brick(BlockKind::Wood, Rgb::new(86, 50, 50), 10);
        painter
            .column(pos.xy(), pos.z - 4..pos.z)
            .sample_with_column(|p, col| p.z > col.riverless_alt as i32)
            .fill(wood_corner);
        match self.lights {
            RoadLights::Default => painter.lanternpost_wood(pos, dir),
            RoadLights::Terracotta => painter.sprite(pos, SpriteKind::LampTerracotta),
        }
    }

    pub fn block(&self, col: &ColumnSample, wpos: Vec3<i32>, dir: Dir) -> Block {
        match self.material {
            RoadMaterial::Dirt => Block::new(
                BlockKind::Earth,
                crate::sim::Path::default()
                    .surface_color((col.sub_surface_color * 255.0).as_(), wpos),
            ),
            RoadMaterial::Cobblestone => Block::new(
                BlockKind::Rock,
                (col.stone_col.as_() * 0.4
                    + (col.sub_surface_color * 0.4 * 255.0)
                    + (RandomField::new(15).get(wpos / (dir.orthogonal().to_vec2() + 1).with_z(1))
                        % 20) as f32)
                    .as_(),
            ),
            RoadMaterial::Sandstone => Block::new(
                BlockKind::Rock,
                (col.stone_col.as_() * 0.2
                    + (col.surface_color * 0.5 * 255.0)
                    + (RandomField::new(15).get(wpos / (dir.orthogonal().to_vec2() + 1).with_z(1))
                        % 20) as f32)
                    .as_(),
            ),
            RoadMaterial::Marble => Block::new(
                BlockKind::Rock,
                Rgb::broadcast((col.marble_small * 0.5 + 0.4) * 255.0).as_(),
            ),
        }
    }
}

/// Represents house data generated by the `generate()` method
pub struct Road {
    pub path: Path<Vec2<i32>>,
    pub kind: RoadKind,
}

impl Structure for Road {
    #[cfg(feature = "use-dyn-lib")]
    const UPDATE_FN: &'static [u8] = b"render_road\0";

    #[cfg_attr(feature = "be-dyn-lib", unsafe(export_name = "render_road"))]
    fn render_inner(&self, site: &Site, land: &Land, painter: &Painter) {
        let field = RandomField::new(76237);

        for p in self.path.iter() {
            if (p.y + p.x) % 3 != 0 {
                continue;
            }

            let current_tile = site.tiles.get(*p);
            let TileKind::Road {
                w,
                a: this_a,
                b: this_b,
                ..
            } = current_tile.kind
            else {
                continue;
            };

            let center = site.tile_center_wpos(*p);

            let light_wpos = |dir: Dir| {
                let width = w as i32 * 2 - 1 - (dir.signum() + 1) / 2;
                center + dir.to_vec2() * width
            };
            let available_dirs: EnumSet<Dir> = Dir::iter()
                .filter(|dir| {
                    let light_wpos = light_wpos(*dir);
                    let tpos = site.wpos_tile_pos(light_wpos);
                    [tpos, tpos + dir.to_vec2()].into_iter().all(|tpos| {
                        let tile = site.tiles.get(tpos);
                        tile.is_natural()
                            || if let TileKind::Road { a, b, .. } = tile.kind {
                                current_tile.plot == tile.plot && this_a == a && this_b == b
                            } else {
                                false
                            }
                    })
                })
                .collect();

            if available_dirs.is_empty() {
                continue;
            }

            let i = field.get(p.with_z(11)) as usize % available_dirs.len();
            let Some(dir) = available_dirs.iter().nth(i) else {
                continue;
            };

            let wpos = light_wpos(dir);

            // TODO: Not sure if this is always correct
            let alt =
                land.get_alt_approx(wpos)
                    .max(land.get_interpolated(wpos, |c| c.water_alt) + 1.0) as i32
                    + 1;
            let wpos = wpos.with_z(alt);
            self.kind.place_light(wpos, -dir, painter);
        }
    }

    fn rel_terrain_offset(&self, col: &ColumnSample) -> i32 {
        (col.riverless_alt as i32).max(col.water_level as i32 + 1)
    }

    fn terrain_surface_at<R: Rng>(
        &self,
        wpos: Vec2<i32>,
        old: Block,
        _rng: &mut R,
        col: &ColumnSample,
        z_off: i32,
        site: &Site,
    ) -> Option<Block> {
        let z = self.rel_terrain_offset(col) + z_off;
        if col.alt < col.water_level && z < 0 {
            return None;
        }

        if z_off <= 0 {
            let tpos = site.wpos_tile_pos(wpos);
            let mut near_roads = LOCALITY.iter().filter_map(|rpos| {
                let tile = site.tiles.get(tpos + rpos);
                if let TileKind::Road { a, b, w } = &tile.kind {
                    if let Some(PlotKind::Road(Road { path, .. })) =
                        tile.plot.map(|p| &site.plot(p).kind)
                    {
                        let is_start = *a == 0;
                        let is_end = *b == path.len() as u16 - 1;
                        let a = path.nodes()[*a as usize];
                        let b = path.nodes()[*b as usize];
                        let path_dir = Dir::from_vec2(b - a);
                        Some((
                            LineSegment2 {
                                start: site.tile_center_wpos(a)
                                    - if is_start {
                                        path_dir.to_vec2() * TILE_SIZE as i32 / 2
                                    } else {
                                        Vec2::zero()
                                    },
                                end: site.tile_center_wpos(b)
                                    + if is_end {
                                        path_dir.to_vec2() * TILE_SIZE as i32 / 2
                                    } else {
                                        Vec2::zero()
                                    },
                            }
                            .as_(),
                            *w,
                        ))
                    } else {
                        None
                    }
                } else {
                    None
                }
            });

            let wposf = wpos.map(|e| e as f32);
            if let Some((line, _)) =
                near_roads.find(|(line, w)| line.distance_to_point(wposf) < *w as f32 * 2.0)
            {
                let dir = Dir::from_vec2((line.start - line.end).as_());
                Some(self.kind.block(col, wpos.with_z(z), dir))
            } else {
                None
            }
        } else if old.is_fluid() || old.kind() == BlockKind::Snow || old.is_terrain() {
            Some(old.into_vacant())
        } else {
            None
        }
    }
}
